Godaddy服務(wù)器上關(guān)于A(yíng)SP.NET網(wǎng)站建設一些經(jīng)驗 - 斷點(diǎn)續傳下載 (二)
發(fā)布時(shí)間:2013-11-22 瀏覽:434打印字號:大中小
談?wù)勗贏(yíng)PS.NET中如何控制文件下載.
設計目的和要求
假設這么一個(gè)應用場(chǎng)景:
一個(gè)主機,上面存有許多文件資料,有各種文件格式.(PDF, DOC, EXE ... 等等).
該主機上運行一個(gè)ASP.NET網(wǎng)站, 用戶(hù)注冊,并付費之后允許他/她下載資料.
文件是放在IIS服務(wù)器上的, 如果用戶(hù)知道具體路徑那么他是可以隨時(shí)下載的. (在沒(méi)有或者不能設置訪(fǎng)問(wèn)權限的情況下.)
如果直接把下載路徑發(fā)送給付費用戶(hù),肯定是行不通的,會(huì )被散播出去. 所以不能把讓客戶(hù)端得知具體路徑,文件內容由 ASP.NET 服務(wù)器頁(yè)面讀取后發(fā)送給客戶(hù)端.
要做的就是: 編寫(xiě)一個(gè)ASP.NET 頁(yè)面服務(wù)器代碼, 讀取指定文件,并發(fā)送給客戶(hù) .
總體思路
.net 里, 有2個(gè)函數可以用來(lái)發(fā)送文件 Response.WriteFile 和 Response.TransmiteFile
它們的主要區別是: WriteFile 是先把文件內容讀取到服務(wù)器緩沖,然后再發(fā)送到客戶(hù)端. 所以對于大文件,會(huì )造成服務(wù)器很大的壓力.
一般用來(lái)處理小文件,比如,發(fā)送給 excel 報表之類(lèi)的. TransmiteFile 不緩沖數據, 直接拋給客戶(hù)端, 所以可以用來(lái)發(fā)大文件.
( 采用 TransmiteFile 來(lái)實(shí)現.)
具體實(shí)現
1. 給客戶(hù)一個(gè)鏈接,形如 http://xxxx/downloads.aspx?Key=ABCD123456
2. 在downloads.aspx的服務(wù)器代碼中, 通過(guò)Key的值,查詢(xún)數據庫,得到服務(wù)器上的真實(shí)文件路徑. 這個(gè)時(shí)候,控制權在 downloads.aspx, 所以可以編寫(xiě)復雜的控制功能, 比如看看用戶(hù)有沒(méi)有登錄,有沒(méi)有付費之類(lèi)的,從而避免外部盜鏈.
3. 得到文件路徑后,調用 Response.TransmiteFile 發(fā)送文件給客戶(hù)端.
4. 因為給客戶(hù)的鏈接里沒(méi)有任何文件名的信息, 所以要在HTTP響應頭里添加一句,告訴客戶(hù)端文件名: Response.AddHeader("Content-Disposition", "attachment; filename=/"" + 你的文件名 + "/""); (如果要支持中文,要考慮編碼的問(wèn)題, 我這里不說(shuō),不是我們的主題.)
5. 如果是一個(gè)大文件, 比如1G, 不支持斷點(diǎn)續傳,是沒(méi)有意義的. 那么如何實(shí)現呢?
(1) 要讓客戶(hù)端知道我們的服務(wù)器支持斷點(diǎn)續傳, 要在HTTP響應頭中包含 Accept-Ranges: bytes 和 ETag: "XXXX".
ETag 是一個(gè)文件的標識, 供客戶(hù)端判斷它請求的是同一個(gè)文件, ETag 的內容在HTTP規范里并沒(méi)有具體要求,只要保證在同一個(gè)服務(wù)器上,同一個(gè)文件有相同的ETag 就行了, 一般就根據文件名和最后修改時(shí)間生成一個(gè)字符串就可以了.
代碼示例:
Response.AddHeader("Accept-Ranges", "bytes"); // 斷點(diǎn)續傳控制.
Response.AddHeader("ETag", "/"" + strETag + "/""); // 允許斷點(diǎn)續傳
(2) 要處理客戶(hù)端請求中的 "Range" 字段. 一般格式是這樣: Range: bytes=1234- 或者 Range: bytes=1234-12345
分別表示從地1235個(gè)字節開(kāi)始下載和下載第1235到第12346個(gè)字節之間的數據.
服務(wù)器首先要添加 Content-Range 響應頭, 然后用 TransmiteFile 發(fā)送指定的數據.
代碼示例:
Response.StatusCode = 206;
Response.AddHeader("Content-Length", (lTo - lFrom + 1).ToString());
Response.AddHeader("Content-Range", string.Format("bytes {0}-{1}/{2}", lFrom, lTo, fi.Length)); // 參數0 和 參數1 是位置. 參數2是文件長(cháng)度
Response.TransmitFile(strFilePath, lFrom, lTo - lFrom + 1);
( 其中, lFrom 和 lTo 是根據客戶(hù)端請求中的 Range 字段得到的.)
說(shuō)一下優(yōu)缺點(diǎn):
1. 可以隨心所欲的控制下載.
2. 可以繞過(guò)服務(wù)器文件類(lèi)型下載的限制, 比如服務(wù)器不允許下載 ISO 和 NRG 文件擴展名的文件, 如果直接輸入RUL會(huì )提示404, 但是用上述的方法可以下載.
3.用這種辦法的話(huà),下載是在.net的一個(gè)線(xiàn)程里做的,如果用戶(hù)量大的話(huà),需要維護多個(gè)響應
附注:
1. TransmitFile(String) ( 函數是 .net 2.0 才加上去的.
2. TransmitFile(String, Int64, Int64) 帶發(fā)送位置參數的重載是 .net 2.0 sp1 以后才支持的. 所以要用本文所說(shuō)的方法實(shí)現斷點(diǎn)續傳, 至少要支持.net 2.0 sp1
3. 沒(méi)有檢測請求頭中的 If-Range 和 Unless-Modified-Since, 如果有需要,在得到文件名之后就可以校驗一下, 分別對應 ETag 和 Last-Modified.
// 1. 獲取服務(wù)器上的文件路徑 // 這里,如果文件路徑有問(wèn)題, 無(wú)法映射則會(huì )拋出異常, strURL 是根據 Key從數據庫中查詢(xún)到的真實(shí)文件路徑
string strFilePath = Server.MapPath("~" + strURL);
// 2. 獲取文件名
string strFileName = System.IO.Path.GetFileName(strFilePath);
// 3. 確認文件是否存在
FileInfo fi = new FileInfo(strFilePath);
if (!fi.Exists)
{
// 退出點(diǎn),文件不存在
}
// 4. 拋給客戶(hù)端
strFileName.Replace(" ", "%20"); // 處理文件名含空格的情況
string strETag = strFileName.ToUpper() + ":" + fi.Length.ToString(); // 我的Etag 是用文件名和字節數構成,馬馬虎虎湊合用.
string strLastTime = fi.LastWriteTimeUtc.ToString("r");
Response.Clear(); // 先把響應流清空
Response.ContentType = "application/octet-stream"; // 指定文件類(lèi)型,使客戶(hù)端總是彈出保存文件的框框.
Response.AddHeader("Content-Disposition", "attachment; filename=/"" + strFileName + "/"");
Response.AddHeader("Accept-Ranges", "bytes"); // 斷點(diǎn)續傳控制.
Response.AddHeader("ETag", "/"" + strETag + "/""); // 允許斷點(diǎn)續傳
Response.AddHeader("Last-Modified", strLastTime);//把最后修改日期寫(xiě)入響應
// 獲取客戶(hù)端請求的范圍, 并且要校驗這個(gè)范圍的有效性
long lFrom = 0;
long lTo = 0;
bool bParts = false;
string strRange = Request.Headers["Range"];
if (ParseRange(strRange, out lFrom, out lTo)) /// ParseRange 是我自己寫(xiě)的函數, 從 Range 中讀取2個(gè)位置.代碼在后面.
{
if (-1 == lFrom && -1 == lTo)
{
// 不允許2個(gè)值都不指定
}
else
{
if (lTo == -1) lTo = fi.Length - 1; // 客戶(hù)端未指定結束位置,則認為是文件的最后一個(gè)字符 Range: bytes=123- 的情況
if (lFrom == -1) // Range: bytes=-123 的情況, 請求最后的123個(gè)字節
{
lFrom = fi.Length - lTo;
lTo = fi.Length - 1;
}
if (lFrom < 0 || lFrom >= fi.Length || lFrom > lTo || lTo < 0 || lTo >= fi.Length)
{
// 以上幾種情況下,范圍的值能解析出來(lái),但是不合法.
// 首先 From 和 To 的下標都應該在文件長(cháng)度范圍內
// 其次 From 應該 <= To
}
else
{
bParts = true;
}
}
}
// 根據用戶(hù)請求,返回數據段或者整個(gè)文件
if(bParts)
{
Response.StatusCode = 206;
Response.AddHeader("Content-Length", (lTo - lFrom + 1).ToString());
Response.AddHeader("Content-Range", string.Format("bytes {0}-{1}/{2}", lFrom, lTo, fi.Length)); // 參數0 和 參數1 是位置,從0開(kāi)始. 參數2是文件長(cháng)度
Response.TransmitFile(strFilePath, lFrom, lTo - lFrom + 1);
}
else
{
Response.AddHeader("Content-Length", fi.Length.ToString());
Response.TransmitFile(strFilePath);
}
Response.End();
}
=============================傳說(shuō)中的分割線(xiàn)======================================
protected bool ParseRange(string strRange, out long lFrom, out long lTo)
{
lFrom = 0;
lTo = 0;
long lTemp = 0;
if (strRange == null || strRange == "")
{
return false; // 字符串為空
}
else
{
strRange = strRange.Replace(" ", ""); // 去除多余的空格
string[] range = strRange.Split(new char[] { '=', '-' });
// 1.分割后,包含3段 第一段是 "Range: bytes", 第二段是起始位置, 第三段是結束位置
if (range.Length != 3)
{
return false; // 格式不正確 只支持 Range: bytes=89294317- 或者 Range: bytes=1234-1235 或者 Range: bytes=-500 3種格式.
}
// 2. 解析起始位置
if (range[1].Length <= 0)
{
// 起始位置未指定
lFrom = -1;
}
else
{
if (!long.TryParse(range[1], out lTemp))
{
return false; // 起始位置無(wú)法解析
}
lFrom = lTemp;
}
// 3. 解析結束位置
if (range[2].Length <= 0)
{
lTo = -1; // 沒(méi)有指定結束位置 Range: bytes=1234- 的情況
}
else
{
if (!long.TryParse(range[2], out lTemp)) // 排除 byte=xxxx- 的情況 TryParse 失敗, 會(huì )把lTemp 置零
{
return false; // 第三度的內容不為空,但是無(wú)法解析
}
lTo = lTemp;
}
return true;
}
}


